package com.baizhi.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.concurrent.TimeUnit;

/*
* @Aspect指明这是一个切面类
*       该类是redis实现缓存的一个切面类  对项目中查询频率高和查询速度慢的数据进行缓存处理
*       需要提供两个方法：
*                   方法1：从redis中查询缓存和添加缓存（先判断  没有要数据时先去数据库中查，并添加到缓存）
*                           因为该方法要有返回值  所有方法1应使用环绕增强
*                   方法2：当redis缓存中的数据被修改时（增删改），应该将对应模块的数据从redis中清空
*                           为了保证缓存中的数据安全（无脏数据）  应该在增删改操作前进行清空
 *        【切点设置：切方法或切注解】
 *        从上面两个增强方法来看，这个切面类的切入点应该采用：注解切入  这样更方便灵活
 *
 * 完成切面类后，在需要查询或删除缓存的方法处（最好是业务方法）添加对应的注解
* @Configuration指明这是一个配置类
* */
@Aspect
@Configuration
@Slf4j
public class RedisCacheAspect {
    /*
    * 一、装配一个java代码操作redis数据库的一个模板类对象  【比直接用原生的Jedis对象方便】
    * */
    @Autowired
    private RedisTemplate redisTemplate;

    /*
    * 二、查询缓存方法
     * 首先，该方法有返回值   所以应该用环绕增强
     * 其次，根据使用的redis数据结构  value是hashMap
     *              redis的key： 注解标注方法所在的类名   【通过连接点对象的getThis()可以获取】
     *                           连接点对象即是该增强方法的形参【ProceedingJoinPoint joinPoint】
     *              hashKey： 注解标注的方法名+参数  【也可以通过连接点对象获取】
     *              hashValue：该redis缓存中的数据  【通过key+hashKey获取】
     *              hashValue若是没有在缓存中找到，则数据库中查询获取【即
     *                      是注解所标注的方法的返回值，该环绕增强方法直接放行即可获得】
     *具体步骤：
     * 1.获取 key 和 hashKey
     * 2.根据 key 和 hashKey 康康redis中有没有数据
     * 3.如果没有  方法放行 查询数据库 将数据库的结果 存储在缓存中  并返回
     * 4.如果有  直接把redis查询的结果返回
     * */
    @Around("@annotation(com.baizhi.annotation.GetCacheAnnotation)")
    public Object getCache(ProceedingJoinPoint joinPoint){
//        1.获取 key 和 hashKey
        //a、获取key
        String key = joinPoint.getThis().toString();
        log.debug("查询缓存时的key="+key);
        //b、获取hashkey   它是由方法名+参数列表组成的字符串  需要拼接（这里用StringBuffer实现）
        StringBuffer buffer = new StringBuffer();
        //先获取方法签名对象
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //再获取方法名  并拼接到buffer中
        String methodName = signature.getMethod().getName();
        buffer.append(methodName);
        //然后获取形参列表
        Object[] args = joinPoint.getArgs();
        //遍历形参列表 并拼接到buffer中
        for (Object arg : args) {
            buffer.append(args.toString());
        }
        //最后获得真正的hashkey   jdk8.0后  字符串拼接用“+”底层用的就是StringBuild
        String hashKey = buffer.toString();
        log.debug("查询缓存时的hashKey="+hashKey);
//        2.根据 key 和 hashKey 康康redis中有没有数据
        //首先，获取一个操作hashMap数据类型的redis的操作符对象
        HashOperations hashOperations = redisTemplate.opsForHash();
        //然后，就可以通过两个key去redis中查询缓存数据
        Object result = hashOperations.get(key, hashKey);
        if(result==null){
//        3.如果查询结果为空  方法放行  获取其切入方法的返回值并将其赋值给result，然后保存到redis缓存中
//        【注意：该增强方法放行 就会去执行其切入的方法  它切入的方法就是查询数据库方法，
//                这又是个环绕增强，所以该方法会获取切入方法的返回值，即查询数据库的结果】
            try {
                result = joinPoint.proceed();//proceed() 该方法就是放行通知
                log.debug("从数据库中查询到的数据："+result);
                //将从数据库中查询的结果和对应的key hashKey保存到redis中
                hashOperations.put(key,hashKey,result);
            } catch (Throwable throwable) {
                log.debug("从数据库中查询结果，保存到redis缓存中出现异常");
            }
            /*
            * 补：一般我们认为缓存数据是不安全的  所以设置一个存活时间（即不手动删除时让redis默认删除操作）
            * redisTemplate中的expire（）方法就是来删除缓存的
            * expire（）方法的参数：
            * 参数1：要删除数据的redis的key
            * 参数2：该redis-key存活的时间
            * 参数3：存活时间的单位
            * */
            redisTemplate.expire(key,120, TimeUnit.SECONDS);
        }
//        4.若查询结果不为空  直接把redis查询的结果返回
        log.debug("最终的查询结果："+result);
        return result;
    }



    /*
    * 三、删除缓存方法（切入点注解标注的也是增删改方法，是有缓存数据的类下的增删改方法）
    *     首先，为了确保缓存中的数据安全（无脏数据）  要在数据库数据修改之前将对应的数据清空
    *           即在增删改操作（切入方法）之前进行   所以这里用前者增强
    *     其次，这里根据redis中的key删除  redis的key设计的是查询方法所在的类的类名  这样执行该增强方法（
    *           即删除缓存时，只删除该类对应的缓存数据）
    * */
    @Before("@annotation(com.baizhi.annotation.RemoveCacheAnnotation)")
    public void removeCache(JoinPoint joinPoint){
        //1、通过切入点对象（注解标注的点） 获取注解标注方法所在的类的类名  即是要删除缓存数据redis的key
        String key = joinPoint.getThis().toString();
        //2、通过模板类对象redisTemplate完成删除redis中的数据
        redisTemplate.delete(key);
    }
}
